Skip to content

docs(skills): objectstack-data factual sweep (2/3) — SKILL.md - #13694

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-13675-data-skill-sweep-2
Aug 31, 2026
Merged

docs(skills): objectstack-data factual sweep (2/3) — SKILL.md#13694
os-zhuang merged 1 commit into
mainfrom
claude/issue-13675-data-skill-sweep-2

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Part of #13675

Program anchor: #13658 — flight ② of the published-skills factual sweep, PR 2 of three at whole-file boundaries. The card and the anchor both stay open; only PR 3 carries a closing keyword. PR 1 is #13687 (rules/field-types.md, rules/relationships.md, rules/indexing.md); this PR is based on origin/main, not stacked on it — the two diffs are disjoint.

Session, for durable attribution: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de

Governed surface — human merge, deliberately parked

skills/** is the published, customer-facing surface. This PR is a draft on purpose: auto-merge is not armed, it is not marked ready, no review is requested, and this seat will not arm it. A draft awaiting the maintainer's own merge is the correct terminal state here, not a stuck PR.

Non-vacuity control — claims proven TRUE by execution

1. The enable capability defaults, all seven at once. The Object Capabilities table gives a default per flag. Parsed enable: {} through ObjectSchema:

enable:{} parsed => {"trackHistory":false,"searchable":true,"apiEnabled":true,
                     "files":false,"feeds":true,"activities":true,"clone":true}

Every value matches the table's Default column — trackHistory false, searchable true, apiEnabled true, files false, feeds true, activities true, clone true.

2. apiMethods is a whitelist over exactly six primitives. All six accepted; the derived verbs are accepted-but-stripped with the platform's own explanation:

ACCEPT | apiMethods:['get'] ['list'] ['create'] ['update'] ['delete'] ['bulk']
ACCEPT | apiMethods:['search'] — with:
  [spec] enable.apiMethods declares retired legacy value(s) [search] — the ApiMethod enum is
  the six primitives get/list/create/update/delete/bulk. Legacy values are stripped at parse;
  their semantics are DERIVED from the primitives:
    • `search`: declare ['list'] (with `searchable` not false) — `search` derives from list ∧ searchable

3. The seed table's four defaults, in one parse. SeedSchema over a minimal seed:

seed parsed => {"object":"status","externalId":"name","mode":"upsert",
                "env":["prod","dev","test"],"records":[{"code":"a"}]}

externalId default 'name', mode default 'upsert', env default ['prod','dev','test'] — exactly the table. The five import modes are also exactly the five listed (upsert|insert|update|ignore|replace); a sixth (delete) is refused by name.

4. The nine lint rules and their severities. Read out of packages/lint/src/data-model-rules.ts, every row matches:

error      relationship/missing-reference
warning    relationship/master-detail-required
suggestion relationship/delete-behavior
suggestion relationship/line-items-inline-edit
suggestion relationship/line-item-should-be-master-detail
warning    relationship/association-inline-edit
suggestion rollup/missing-summary
warning    field/select-missing-options
suggestion object/missing-name-field

5. The R9-vs-ADR-0079 callout, the subtlest claim in the file. It asserts two deliberately different "name-like" sets. Both measured:

lint  NAME_LIKE_FIELDS = ['name','title','subject','label','full_name','display_name','code']
spec  exactOrder       = ['name','title','subject','label','full_name','display_name']

code is in the lint set and absent from the title-derivation set — exactly as the callout says.

6. The strict/closed surfaces, each with a live control. namespace, permissions, hooks, workflows, stateMachines, softDelete are all refused by ObjectSchema with a located message (the skill claims each). permissionSets: on the stack is refused and renamed for you:

REJECT | defineStack({permissionSets}) | Unrecognized key(s) on this stack definition:
         `permissionSets`. Did you mean `permissionSets` → `permissions`?

tenancy.strategy is refused ("removed from @objectstack/spec after v15.0 — it never had a consumer"). The protection block is .strict() with reason required at 1–500 chars and exactly four lock levels (read-only refused). fieldGroups[].collapse is exactly none|expanded|collapsed and a non-snake_case group key is refused. readScope is exactly own|own_and_reports|unit|unit_and_below|org; RLS operation is exactly select|insert|update|delete|all, object really is required per policy, and the plural operations is refused with a rename hint. All seven permission bits (allowCreate/allowRead/allowEdit/allowDelete/allowTransfer/viewAllRecords/modifyAllRecords) parse green, while an invented allowFrobnicate is refused — the control that makes the seven greens mean something. objectExtensions[].priority is .min(0).max(999).default(200), byte-exact with the stated "default 200; range 0–999".

The three FALSE claims, and what measured them

落点 before after
L81 (Object properties table) "titleFormatRetired (ADR-0079) — a render-only template the server can't return or query. Use nameField; …" "titleFormatDeprecated, not removed (ADR-0079) — render-only: the server can't return or query it. Use nameField (it wins); …"
L786-787 (tenancy example) "tenantField: 'tenant_id', // default: 'tenant_id'" "// tenantField — NO default; omit it and the driver uses organization_id``"
L743-745 and L979 (Sensitive fields; Advanced Features note) "The former encryptionConfig and maskingRule field keys were pruned from FieldSchema — they had no runtime consumer (dead surface; setting them protected nothing)." / "the encryptionConfig / maskingRule field keys were pruned" "The former encryptionConfig field key was pruned from FieldSchema — it had no runtime consumer. maskingRule is live (plugin-security's FieldMasker enforces it)." / "the encryptionConfig field key was pruned"

1. titleFormat is deprecated, not retired

The skill's own vocabulary makes this a distinction with teeth: it uses "Retired" for keys that are now a parse error (indexes[].type, indexes[].partial — "Both are now a tsc error and a parse error"), and one row above titleFormat it correctly writes "Deprecated alias for nameField (still honored as a fallback)" for displayNameField. So a reader takes "Retired" to mean authoring it fails the build. Measured:

ACCEPT | titleFormat

It is a declared, optional ObjectSchema key — packages/spec/src/data/object.zod.ts:2076 describes it as [DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. It is still read (packages/objectql/src/registry.ts:1617 names resolveDisplayField / titleFormat as the live read path for title-less system tables) and it still has a semantic migration entry (packages/spec/src/migrations/entries/semantic/11.object-titleFormat-to-nameField.ts). The control is the namespace row in the same table, which really is retired and really does reject:

REJECT | namespace | Unrecognized key(s) on this object: `namespace`.
  • `namespace` was retired (ADR-0129 D3) — the object `name` IS the canonical id everywhere

The correction keeps the advice (use nameField) and fixes the mechanism, adding the part an author needs: nameField takes precedence.

2. tenancy.tenantField has no default, and 'tenant_id' is the wrong column

The example wrote tenantField: 'tenant_id' with the comment // default: 'tenant_id'. Measured:

tenancy:{enabled:true} parsed => {"enabled":true}

No tenantField materializes. The schema's own docblock (object.zod.ts:575) records that the default was removed and why, and the reason is that the documented value was actively wrong:

tenantField carries no default (#5315). It used to default to 'tenant_id', which no consumer could act on: the platform's tenant column is organization_id (kernel-injected; the same column tenantPolicy() in security/rls.zod.ts and the RLS predicates assume), and the SQL driver's computeTenantField honours a declared name only when the object actually has that field — so the materialized 'tenant_id' merely sent it looking for a column that did not exist before falling back to organization_id anyway.

This mattered inside this very file: three sections below, the RLS and visibility-posture recipes correctly use organization_id throughout, so the tenancy example was teaching a column name the rest of the page contradicts. Note the docblock also records that tenant is a word ADR-0120 refuses for the authorable vocabulary. The corrected comment names the fallback column instead of inventing a default.

3. maskingRule was never pruned — it is a live, enforced capability

The Sensitive-fields section grouped maskingRule with encryptionConfig as "pruned from FieldSchema", "dead surface; setting them protected nothing", and the Advanced Features note repeated it. Measured — and the two keys behave in opposite ways:

REJECT | text + encryptionConfig | Unrecognized key(s) on this field: `encryptionConfig`.
         • pruned in 2026-06 — it implied at-rest protection that never happened.
           The real channel is `type: 'secret'`.
ACCEPT | text + maskingRule: 'phone'                 | out="phone"
ACCEPT | text + maskingRule: 'email'                 | out="email"
ACCEPT | text + maskingRule: { keepHead:3, keepTail:4 } | out={"keepHead":3,"keepTail":4}

encryptionConfig really was pruned, with its own tombstone message. maskingRule parses, retains its value, and is declared in field.zod.ts as "Partial masking rule enforced by the runtime FieldMasker (single channel — API, UI, export and AI context all see the same masked value)", with named presets (phone 138****5678, id_card, bank_account, email, name) or { keepHead, keepTail }. Its consumers are real and plural: packages/plugins/plugin-security/src/field-masker.ts and its dedicated field-masking-rule.test.ts, explain-engine.ts, security-plugin.ts, packages/plugins/plugin-approvals/src/payload-redaction.ts, packages/plugins/plugin-audit/src/read-audit.ts, a dogfood test, and the metadata form modules with translations in four locales.

This is the most consequential of the three: the skill was telling authors that a shipped, enforced data-protection capability does not exist and "protected nothing". Under the ratchet the correction is one clause, so the full maskingRule reference (presets, the unmask gate, the interaction with requiredPermissions) is recorded as a content/docs/** follow-up rather than expanded here.

Written to the token ratchet, not around it

SKILL.md had 6 tokens of headroom, and the corrections needed more than that, so they are paid for by genuine deletion inside the same file — chiefly the "dead surface; setting them protected nothing" clause, which described a key that no longer exists. No ceiling was raised; that is a maintainer's call, not a dev's.

reading before after
SKILL.md lines (whole file) 1210 1210
SKILL.md tokens (ceiling 13783) 13783 13782

The ratchet's own verdict line at head 0ccf1b1e7:

✓ check-skills-token-ratchet: skills/objectstack-data/SKILL.md is 13782 tokens (ceiling 13783; headroom 1).
✓ check-skills-token-ratchet: 38 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.

Gates — derived, not recalled, run locally at head 0ccf1b1e7

Family derived from the real diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, harvested with --commands. It derives the same 13 families as PR 1. 12 green, 1 NOT MEASURED; exit codes captured before any pipe:

EXIT=0 :: node scripts/check-ci-filter-parity.mjs
EXIT=0 :: node scripts/check-cross-package-test-inputs.mjs
EXIT=0 :: node scripts/check-shard-attestation.mjs
EXIT=0 :: node scripts/check-skills-token-ratchet.mjs
EXIT=3 :: node scripts/check-test-completeness.mjs          <-- PREREQUISITE NOT MET
EXIT=0 :: pnpm --filter @objectstack/lint run check:doc-formula-expressions
EXIT=0 :: pnpm check:agent-test-spelling
EXIT=0 :: pnpm check:cross-package-test-inputs
EXIT=0 :: pnpm check:doc-authoring
EXIT=0 :: pnpm check:pm-governed-merges
EXIT=0 :: pnpm check:role-word
EXIT=0 :: pnpm check:skill-compatibility
EXIT=0 :: pnpm check:skill-frame-sync

Two families beyond the derived list, because AGENTS.md names them for this exact edit and the derivation did not. AGENTS.md's "Touched packages/spec? Regenerate its artifacts" table has the row "A SKILL.md (frontmatter or body) → check:skill-docs, check:skill-refs". The path derivation named neither, so they were run anyway rather than trusted to be irrelevant:

EXIT=0 :: pnpm --filter @objectstack/spec check:skill-docs    ✓ skills/README.md
                                                              ✓ content/docs/ai/skills-reference.mdx
                                                              ✅ Skill docs in sync
EXIT=0 :: pnpm --filter @objectstack/spec check:skill-refs    ✅ 9 generated files in sync with packages/spec

Both are green because this diff changes body prose only, not front-matter — but that is a measurement, not an assumption, and it is the one this PR could most plausibly have broken.

check:skill-examples is green and load-bearing here, unlike in PR 1: SKILL.md carries 4 os:check blocks. It requires @objectstack/client-react to be built (it refuses rather than degrading), so that closure was built first and the gate re-run:

✅ 260 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them
   260 marked example(s) across 101 file(s), 3 surface(s)

check-test-completeness.mjs exits 3 with its own text "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named". Recorded as NOT MEASURED, not as a red.

check-nul-bytes clean: OK (scanned 7557 text file(s) … no raw ASCII control bytes), plus a direct control-byte scan over SKILL.md with no hits.

ESLint was not run repo-wide, and the narrowing is measured. Same three readings as PR 1, re-taken for this file: (1) no files: block in eslint.config.mjs names .md, and the strings .md/markdown appear nowhere in it; (2) --format json over SKILL.md returns 1 result, errorCount: 0, one message with ruleId: null reading "File ignored because no matching configuration was supplied."; (3) type-aware linting is not enabled anywhere (no parserOptions.project, which the config's own line 327 states), so this diff cannot move a verdict on an untouched file.

No changeset

This PR releases nothing from any package — a pure skills/** documentation correction. skip-changeset is applied.

Calibration numbers for the anchor

Same counting rule as PR 1: one claim per assertable proposition about platform behaviour. Of SKILL.md's 1,210 lines, ~250 are out of the inventory (front-matter, the Skill Boundaries routing table, "When to Use This Skill", the Quick Reference link list, section headings, cross-skill pointers, Common Patterns restatements of rules/** already inventoried in PR 1).

281 behavioral claims inventoried in SKILL.md. 3 FALSE (1.1%), ~29 NOT MEASURABLE, the rest VERIFIED with executed probes wherever behaviour-bearing.

The concentration reading from PR 1 holds and sharpens. By markdown rendering, 1 of 3 falsehoods is a table cell (the titleFormat row); the other two are a code-comment and a prose sentence. By content class, 3 of 3 are surface/liveness claims — "is this key retired, deprecated, or live?" All three are the same error in the same direction: the document declares a surface deader than it is. That is the mirror image of flight ①'s dominant flavour (rows outliving a removal), and it is arguably the more dangerous one, because it hides a shipped capability (maskingRule) and a live fallback (titleFormat) instead of merely advertising a dead one.

Running total for flight ② so far: 595 claims inventoried, 9 FALSE (1.5%) across 4 of 12 files and 2,429 of 4,935 lines.

What is not here, on purpose

  • No implementation was touched.
  • Probes were written in the worktree, run, and deleted — evidence, not commits.
  • One observation recorded rather than acted on: SEARCHABLE_ENUM_TYPES in packages/spec/src/data/search-fields.ts is new Set(['select', 'status']), and status is not a member of the 49-value FieldType enum. The skill reproduces the implementation's list faithfully (so its sentence is VERIFIED, not false); the dead vocabulary entry is an implementation-side observation for the PM, not something this PR should change.
  • The check:skill-examples build prerequisite (@objectstack/client-react and its service closure) was satisfied for real rather than skipped, because this file's 4 os:check blocks are exactly what that gate exists to type-check.

Generated by Claude Code

Three measured-false behavioral claims corrected against the implementation:

- titleFormat is labelled 'Retired (ADR-0079)' but is DEPRECATED: still a
  declared ObjectSchema key, still parses, still read by objectql's registry.
- tenancy.tenantField is shown with '// default: tenant_id'. It carries NO
  default; the platform tenant column is organization_id.
- maskingRule is claimed 'pruned from FieldSchema' (twice). It is live, with a
  runtime enforcer in plugin-security's FieldMasker.

Token ratchet: SKILL.md shrinks (13783 -> 13782).

Part of #13675

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de
@os-zhuang
os-zhuang marked this pull request as ready for review August 31, 2026 07:16
@os-zhuang
os-zhuang enabled auto-merge August 31, 2026 07:16
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 31, 2026
Merged via the queue into main with commit b8fc728 Aug 31, 2026
34 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-13675-data-skill-sweep-2 branch August 31, 2026 07:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants